/*

 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * The software in this package is published under the terms of the GPL v2.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE-GPL.txt file.
 */

package org.cemantika.management.drools;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManagerFactory;

import org.cemantika.contextconsumer.ContextConsumerAdapter;
import org.cemantika.exception.KnowledgeSessionAlreadyCreatedException;
import org.cemantika.exception.KnowledgeSessionNotCreatedException;
import org.cemantika.exception.LoadKnowledgeSessionException;
import org.cemantika.infrastructure.model.ContextUser;
import org.cemantika.infrastructure.model.PersistentEntity;
import org.drools.KnowledgeBase;
import org.drools.KnowledgeBaseFactory;
import org.drools.marshalling.MarshallerFactory;
import org.drools.marshalling.ObjectMarshallingStrategy;
import org.drools.persistence.jpa.JPAKnowledgeService;
import org.drools.runtime.Environment;
import org.drools.runtime.EnvironmentName;
import org.drools.runtime.StatefulKnowledgeSession;
import org.drools.runtime.process.ProcessInstance;
import org.drools.runtime.rule.FactHandle;
import org.springframework.orm.jpa.JpaTransactionManager;

/**
 * DroolsContextualManipulation represents the manipulation of the contextual information according
 * to the framework Drools.
 * 
 * @author andersonfellipe
 */
@SuppressWarnings("restriction")
public class DroolsContextualManipulation extends PersistentEntity implements IDroolsContextualManipulation, KnowledgeSessionLookup, Runnable, Serializable {

    /**
     * Represents the serial id of the entity.
     */
    private static final long serialVersionUID = 888965989595261470L;
    /**
     * Represents the unique identifier of the {@link DroolsContextualManipulation}.
     */
    private Integer id;
    /**
     * Represents the knowledge base of the framework Drools.
     */
    private KnowledgeBase knowledgeBase;
    /**
     * Represents the session of the framework Drools.
     */
    private StatefulKnowledgeSession statefulSession;
    /**
     * Represents the entity manager factory used by Drools to persist the session.
     */
    private EntityManagerFactory entityManagerFactory;
    /**
     * Represents the JPA transaction manager used by Drools to manipulate the transactions.
     */
    private JpaTransactionManager jpaTransactionManager;
    /**
     * Represents the environment configured to the entity manager and JPA transaction manager.
     */
    private Environment environment;
    /**
     * Represents the context user that will be associated to a unique session.
     */
    private ContextUser contextUser;
    /**
     * Represents the list of {@link FactHandle} and the foci they are related with.
     */
    private Map<ContextConsumerAdapter, FactHandle> facts;
    /**
     * Represents the list with the class name of each focus inserted into Drools session.
     */
    private List<String> factTypes;

    /**
     * Creates a new instance of DroolsContextualManipulation.
     */
    public DroolsContextualManipulation() {
        facts = new HashMap<ContextConsumerAdapter, FactHandle>();
        factTypes = new LinkedList<String>();
    }

    /**
     * Used to configure a bean of type {@link DroolsContextualManipulation} with Spring for
     * persistence purposes.
     */
    public void initPersistenceConfig() {
        if (knowledgeBase != null && entityManagerFactory != null && jpaTransactionManager != null) {
            environment = KnowledgeBaseFactory.newEnvironment();
            environment.set(EnvironmentName.ENTITY_MANAGER_FACTORY, entityManagerFactory);
            environment.set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES, new ObjectMarshallingStrategy[] {
                MarshallerFactory.newSerializeMarshallingStrategy()
            });
            environment.set(EnvironmentName.TRANSACTION_MANAGER, jpaTransactionManager);
        }
    }
    
    /**
     * Add a simple fact object into session.
     * 
     * @param factObject the object of the fact to add.
     * @throws KnowledgeSessionNotCreatedException
     */
    public void addFact(Object factObject) throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.insert(factObject);
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }
    
    /**
     * Remove a simple fact object from session.
     * 
     * @param factObject the object of the fact to remove.
     * @throws KnowledgeSessionNotCreatedException
     */
    public void removeFact(Object factObject) throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.retract(statefulSession.getFactHandle(factObject));
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }
    
    /**
     * Remove all fact objects from session.
     * 
     * @throws KnowledgeSessionNotCreatedException
     */
    public void removeAllFactsAndFoci() throws KnowledgeSessionNotCreatedException {
        try {
            Collection<FactHandle> factHandles = statefulSession.getFactHandles();
            for (FactHandle factHandle : factHandles) {
                statefulSession.retract(factHandle);
            }
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }
    
    /**
     * Update of a simple fact object from session.
     * 
     * @param factObject the object of the fact to update.
     * @throws KnowledgeSessionNotCreatedException
     */
    public void updateFact(Object factObject) throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.update(statefulSession.getFactHandle(factObject), factObject);
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#newKnowledgeSession()
     */
    @Override
    public int newKnowledgeSession() throws KnowledgeSessionAlreadyCreatedException {
        if (statefulSession != null) {
            throw new KnowledgeSessionAlreadyCreatedException();
        }
        if (knowledgeBase != null && entityManagerFactory != null && jpaTransactionManager != null) {
            statefulSession = JPAKnowledgeService.newStatefulKnowledgeSession(knowledgeBase, null, environment);
        } else {
            statefulSession = knowledgeBase.newStatefulKnowledgeSession();
        }
        statefulSession.fireUntilHalt();
        return statefulSession.getId();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#loadKnowledgeSession(int)
     */
    @Override
    public void loadKnowledgeSession(int sessionId) throws LoadKnowledgeSessionException {
        if (knowledgeBase != null && entityManagerFactory != null && jpaTransactionManager != null) {
            try {
                statefulSession =
                        JPAKnowledgeService.loadStatefulKnowledgeSession(sessionId, knowledgeBase, null, environment);
                statefulSession.fireUntilHalt();
            } catch (RuntimeException e) {
                throw new LoadKnowledgeSessionException(sessionId);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#addFocus(java.lang.Object)
     */
    @Override
    public void addFocus(ContextConsumerAdapter focus) throws KnowledgeSessionNotCreatedException {
        try {
            FactHandle fact = statefulSession.insert(focus);
            facts.put(focus, fact);
            factTypes.add(focus.getClass().getSimpleName());
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#removeFocus(java.lang.Object)
     */
    @Override
    public void removeFocus(ContextConsumerAdapter focus) throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.retract(facts.get(focus));
            facts.remove(focus);
            factTypes.remove(focus.getClass().getSimpleName());
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#updateFocus(java.lang.Object)
     */
    @Override
    public void updateFocus(ContextConsumerAdapter focus) throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.update(facts.get(focus), focus);
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.cemantika.management.ContextualManipulation#isFocusRegistered(org.cemantika.contextconsumer
     * .ContextConsumerAdapter)
     */
    public boolean isFocusRegistered(ContextConsumerAdapter focus) {
        return facts.containsKey(focus);
    }

    /* (non-Javadoc)
     * @see org.cemantika.management.ContextualManipulation#hasKnowledgeSession()
     */
    @Override
    public boolean hasKnowledgeSession() {
        return (statefulSession != null);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#terminate()
     */
    @Override
    public void terminate() throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.dispose();
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.ContextualManipulation#clearContextualMemory()
     */
    @Override
    public void clearContextualMemory() throws KnowledgeSessionNotCreatedException {
        try {
            Collection<FactHandle> facts = statefulSession.getFactHandles();
            for (FactHandle fact : facts) {
                statefulSession.retract(fact);
            }
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.drools.IDroolsContextualManipulation#getFactTypes()
     */
    public List<String> getFactTypes() {
        return factTypes;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.cemantika.management.drools.IDroolsContextualManipulation#hasFocusOfType(java.lang.String
     * )
     */
    public boolean hasFocusOfType(String focusType) {
        return factTypes.contains(focusType);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.cemantika.management.drools.IDroolsContextualManipulation#startRules()
     */
    public void startRules() throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.fireAllRules();
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /* (non-Javadoc)
     * @see org.cemantika.management.drools.IDroolsContextualManipulation#startRulesForever()
     */
    public void startRulesForever() throws KnowledgeSessionNotCreatedException {
        
        if (statefulSession == null) {
            throw new KnowledgeSessionNotCreatedException();
        }
        
        Thread fireUntilHaltThread = new Thread(this);
        fireUntilHaltThread.start();
    }

    /* (non-Javadoc)
     * @see org.cemantika.management.drools.IDroolsContextualManipulation#stopRules()
     */
    public void stopRules() throws KnowledgeSessionNotCreatedException {
        try {
            statefulSession.halt();
        } catch (NullPointerException e) {
            throw new KnowledgeSessionNotCreatedException();
        }
    }

    /**
     * Represents the call to the Drools session fireUntilHalt method because the execution 
     * stops in this line until the halt method is called
     */
    @Override
    public void run() {
        statefulSession.fireUntilHalt();
    }

    /**
     * Get the id value.
     * 
     * @return the id
     */
    public Integer getId() {
        return id;
    }

    /**
     * Assign the value of the field id.
     * 
     * @param id the id to set
     */
    public void setId(Integer id) {
        this.id = id;
    }

    /**
     * Get the statefulSession value.
     * 
     * @return the statefulSession.
     */
    public StatefulKnowledgeSession getStatefulSession() {
        return statefulSession;
    }

    /**
     * Assign the value of the field statefulSession.
     * 
     * @param statefulSession the statefulSession to set.
     */
    public void setStatefulSession(StatefulKnowledgeSession statefulSession) {
        this.statefulSession = statefulSession;
    }

    /**
     * Get the knowledgeBase value.
     *
     * @return the knowledgeBase.
     */
    public KnowledgeBase getKnowledgeBase() {
        return knowledgeBase;
    }

    /**
     * Assign the value of the field knowledgeBase.
     * 
     * @param knowledgeBase the knowledgeBase to set.
     */
    public void setKnowledgeBase(KnowledgeBase knowledgeBase) {
        this.knowledgeBase = knowledgeBase;
    }

    /**
     * Get the entityManagerFactory value.
     *
     * @return the entityManagerFactory.
     */
    public EntityManagerFactory getEntityManagerFactory() {
        return entityManagerFactory;
    }

    /**
     * Assign the value of the field entityManagerFactory.
     * 
     * @param entityManagerFactory the entityManagerFactory to set.
     */
    public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
        this.entityManagerFactory = entityManagerFactory;
    }

    /**
     * Get the jpaTransactionManager value.
     *
     * @return the jpaTransactionManager.
     */
    public JpaTransactionManager getJpaTransactionManager() {
        return jpaTransactionManager;
    }

    /**
     * Assign the value of the field jpaTransactionManager.
     * 
     * @param jpaTransactionManager the jpaTransactionManager to set.
     */
    public void setJpaTransactionManager(JpaTransactionManager jpaTransactionManager) {
        this.jpaTransactionManager = jpaTransactionManager;
    }

    /**
     * Get the environment value.
     *
     * @return the environment.
     */
    public Environment getEnvironment() {
        return environment;
    }

    /**
     * Assign the value of the field environment.
     *
     * @param environment the environment to set.
     */
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * Get the contextUser value.
     * 
     * @return the contextUser.
     */
    public ContextUser getContextUser() {
        return contextUser;
    }

    /**
     * Assign the value of the field contextUser.
     * 
     * @param contextUser the contextUser to set.
     */
    public void setContextUser(ContextUser contextUser) {
        this.contextUser = contextUser;
    }

    /**
     * Get the facts value.
     *
     * @return the facts.
     */
    public Map<ContextConsumerAdapter, FactHandle> getFacts() {
        return facts;
    }

}
